package com.qiusuo0226.platform.aspect;

import cn.hutool.crypto.SecureUtil;
import com.qiusuo0226.common.util.AspectTool;
import com.qiusuo0226.http.util.HttpServletRequestTool;
import com.qiusuo0226.platform.annotation.RequestTimeLimit;
import com.qiusuo0226.redis.util.RedisTool;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.checkerframework.checker.units.qual.A;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;

/**
 * 接口请求次数限制切面逻辑
 * @author qiusuo
 * @date 2023/3/22 18:02
 */
@Slf4j
@Component
@Aspect
@Order(1)
public class RequestTimeLimitAspect {

    /**
     * 切入点
     * 匹配使用了RequestTimeLimit注解的方法
     */
    @Pointcut("@annotation(com.qiusuo0226.platform.annotation.RequestTimeLimit)")
    private void pt() {}

    @Autowired
    private RedisTool redisTool;

    /**
     * 通知
     * @return
     * @throws Throwable
     */
    @Around("pt()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        // 获取请求的基本信息
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        // 获取切点的基本信息对象
        AspectTool instance = AspectTool.getInstance(point);

        RequestTimeLimit methodAnnotation = instance.getMethodAnnotation(RequestTimeLimit.class);


        boolean b = validRequestCount(HttpServletRequestTool.getRequestURL(request),
                HttpServletRequestTool.getRemoteHost(request),
                methodAnnotation.count(),
                methodAnnotation.time());

        if (b) {
            // 执行切点方法，万一原方法存在异常，也从此处抛出
            Object res = point.proceed();
            return res;
        } else {
            throw new Exception("访问过于频繁，请稍候再试");
        }
    }

    @AfterThrowing(value = "pt()", throwing = "e")
    public void afterThrowing(Throwable e) {
        log.error("RequestTimeLimitAspect error ", e);
    }

    /**
     * 判断某一个ip请求接口的次数在指定的时间内是否超过限制
     *
     * @param ipAddr ip地址
     * @param uri 请求地址
     * @param limitCount 限制次数
     * @param timeOut 限制时间
     * @return false: 超过限制次数，true: 未超过限制次数
     */
    private boolean validRequestCount(String ipAddr, String uri, int limitCount, long timeOut) {
        try {
            // 通过IP和请求的接口地址拼接成的缓存key
            String redisKey = "req_limit_".concat(ipAddr).concat(uri);
            // 将 req_limit_IP地址接口地址 字符串进行MD5加密做为key，第一安全第二缩减长度
            String redisMd5key = SecureUtil.md5(redisKey);
            // 拼完整的缓存key（地址）
            String wholeKey = "request:timeLimit:".concat(redisMd5key);
            try {
                // 接口访问数量 + 1
                long count = redisTool.incr(wholeKey, 1);
                // 当该IP在该时间段第一次请求该接口的时候，初始化它的有效时间
                if (count == 1) {
                    redisTool.expire(wholeKey, timeOut, TimeUnit.MILLISECONDS);
                }
                // 当接口访问数量大于限制次数的时候，返回请求失败
                if (count > limitCount) {
                    return false;
                }
            } catch (Exception e) {
                log.warn("RequestTimeLimitAspect validRequestCount warn: {} ","无法进行接口限流功能，可能是Redis连接不上，默认连接127.0.0.1:6379！！！");
                log.error("RequestTimeLimitAspect validRequestCount error:", e);
                // 虽然无法进行限流，但接口不代表功能出现问题，所以返回true，让逻辑代码继续执行。
                return true;
            }

        } catch (Exception e) {
            log.error("RequestTimeLimitAspect validRequestCount errore", e);
            return false;
        }
        return true;
    }
}
